package br.mikhas.util;

import static br.mikhas.util.Assert.notEmpty;
import static br.mikhas.util.Assert.notNull;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * Utility methods to help working with IO operations
 * 
 * @author Mikhail Domanoski
 * 
 */
public class IOUtils {
	/**
	 * Default byte buffer size
	 */
	private static final int BUFFER_SIZE = 128 * 1024;// 128 Kilo Bytes

	private IOUtils() {
	}

	/**
	 * Copies the stream from one {@link Reader} to a {@link Writer}
	 * 
	 * @param in
	 *            The input reader
	 * @param out
	 *            The output writer
	 */
	public static void copy(Reader in, Writer out) {
		notNull(in);
		notNull(out);

		char[] buffer = new char[BUFFER_SIZE];
		int count = 0;

		try {
			while ((count = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
				out.write(buffer, 0, count);
			}

			close(in);
			close(out);

		} catch (IOException e) {
			ExceptionUtils.throwWrapped(e);
		}
	}

	/**
	 * Copies teh content of one file to another
	 * 
	 * @param in
	 *            The file to be copied
	 * @param out
	 *            The destination of the file
	 * @throws IOException
	 */
	public static void copy(File in, File out) {
		notNull(in);
		notNull(out);

		OutputStream fos = outputStream(out);
		InputStream fis = inputStream(in);

		internalCopy(fis, fos, true);

	}

	/**
	 * Copies data from an {@link InputStream} to an {@link OutputStream}
	 * 
	 * @param in
	 *            The source of the data
	 * @param out
	 *            The destiny of the data
	 */
	public static void copy(InputStream in, OutputStream out) {
		internalCopy(in, out, true);
	}

	/**
	 * Copies data from an {@link InputStream} to an {@link OutputStream}
	 * 
	 * @param in
	 *            The source of the data
	 * @param out
	 *            The destiny of the data
	 * @param close
	 *            Flags if the streams shoud be closed after copy
	 */
	private static void internalCopy(InputStream in, OutputStream out,
			boolean close) {
		notNull(in);
		notNull(out);

		byte[] buffer = new byte[BUFFER_SIZE];
		int count;

		try {

			while ((count = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
				out.write(buffer, 0, count);
			}

		} catch (IOException e) {
			ExceptionUtils.throwWrapped(e);
		} finally {
			if (close) {
				close(in);
				close(out);
			}
		}
	}

	/**
	 * Saves an input stream data in a zip file.
	 * <p>
	 * The name of the entry will be the same as the zipfile name.
	 * 
	 * @param in
	 *            The the file to be compressed
	 * @param zipfile
	 *            The output zip file
	 */
	public static void zip(File in, File zipfile) {
		notNull(in);
		if (in.exists() && in.isFile()) {
			InputStream input = inputStream(in);
			zip(input, in.getName(), zipfile);
		}
	}

	/**
	 * Saves an input stream data in a zip file.
	 * <p>
	 * The name of the entry will be the same as the zipfile name.
	 * 
	 * @param in
	 *            The the file to be compressed
	 * @param zipfile
	 *            The output zip file
	 */
	public static void zip(File in, String entryName, File zipfile) {
		notNull(in);
		if (in.exists() && in.isFile()) {
			InputStream input = inputStream(in);
			zip(input, entryName, zipfile);
		}
	}

	/**
	 * Saves an input stream data in a zip file
	 * 
	 * @param in
	 *            The input stream which contains the data
	 * @param entryName
	 *            The name of the entry to be zipped
	 * @param zipfile
	 *            The output zip file
	 */
	public static void zip(InputStream in, String entryName, File zipfile) {
		notNull(in);
		notNull(zipfile);
		notEmpty(entryName);

		try {
			ZipOutputStream zip = new ZipOutputStream(outputStream(zipfile));

			zip.putNextEntry(new ZipEntry(entryName));

			internalCopy(in, zip, true);
		} catch (IOException e) {
			ExceptionUtils.throwWrapped(e);
		}
	}

	/**
	 * Unzips a zip file to another file
	 * 
	 * @param zipfile
	 *            The zip file to extracts
	 * @param out
	 *            The output file to unzip data
	 */
	public static void unzip(File zipfile, File out) {
		unzip(zipfile, out.getName(), outputStream(out));
	}

	/**
	 * Unzips a zip file to another file
	 * 
	 * @param zipfile
	 *            The zip file to extracts
	 * @param entryName
	 *            the name of the entry to extract
	 * @param out
	 *            The output file to unzip data
	 */
	public static void unzip(File zipfile, String entryName, File out) {
		unzip(zipfile, entryName, outputStream(out));
	}

	/**
	 * Unzips a zip file and transfer to an {@link OutputStream}
	 * 
	 * @param zipfile
	 *            The zip file to extracts
	 * @param out
	 *            The output stream to transfer the data
	 */
	public static void unzip(File zipfile, OutputStream out) {
		unzip(zipfile, zipfile.getName(), out);
	}

	/**
	 * Unzips a zip file and transfer to an {@link OutputStream}
	 * 
	 * @param zipfile
	 *            The zip file to extracts
	 * @param entryName
	 *            The name of the zip entry
	 * @param out
	 *            The output stream to transfer the data
	 */
	public static void unzip(File zipfile, String entryName, OutputStream out) {
		notEmpty(entryName);
		notNull(out);
		notNull(zipfile);
		if (zipfile.exists() && zipfile.isFile())
			try {
				ZipInputStream zip = new ZipInputStream(inputStream(zipfile));

				ZipEntry entry;

				while ((entry = zip.getNextEntry()) != null) {
					if (entryName.equals(entry.getName())) {
						internalCopy(zip, out, true);
						return;
					}
				}

			} catch (IOException e) {
				ExceptionUtils.throwWrapped(e);
			}
	}

	/**
	 * Null-safe resource flushing and closing.
	 * 
	 * @param closeable
	 *            resource
	 */
	public static void close(Closeable closeable) {
		if (closeable != null)
			try {
				if (closeable instanceof Flushable) {
					try {
						((Flushable) closeable).flush();
					} catch (Throwable tt) {
					}
				}
				closeable.close();
			} catch (Throwable t) {

			}
	}

	/**
	 * Reads content from a {@link File} and returns its content as a String
	 * 
	 * @param file
	 *            The file to read
	 * @return The file content
	 */
	public static String read(File file) {
		try {
			return read(new FileReader(file));
		} catch (FileNotFoundException e) {
			throw ExceptionUtils.wrapChecked(e);
		}
	}

	/**
	 * Reads the content from an {@link InputStream} and returns its content as
	 * a String
	 * 
	 * @param input
	 *            The input stream to read data
	 * @return The input stream content
	 */
	public static String read(InputStream input) {
		return read(new InputStreamReader(input));
	}

	/**
	 * Reads the content from a {@link Reader} and returns it as a String
	 * 
	 * @param reader
	 *            The reader to read the content
	 * @return The {@link Reader} content
	 */
	public static String read(Reader reader) {
		Assert.notNull(reader);

		char[] buffer = new char[BUFFER_SIZE];
		StringWriter writer = new StringWriter();
		int read = 0;

		try {
			while ((read = reader.read(buffer)) > 0)
				writer.write(buffer, 0, read);
		} catch (IOException e) {
			ExceptionUtils.throwWrapped(e);
		}

		return writer.toString();
	}

	/**
	 * Writes the given string to the File
	 * 
	 * @param text
	 *            The text to write
	 * @param output
	 *            The output file to write the text
	 */
	public static void write(String text, File output) {
		write(text, outputStream(output));
	}

	/**
	 * Writes the given string to the {@link OutputStream}
	 * 
	 * @param text
	 *            The text to write
	 * @param output
	 *            The output to write the text
	 */
	public static void write(String text, OutputStream output) {
		if (!(output instanceof BufferedOutputStream))
			output = new BufferedOutputStream(output, BUFFER_SIZE);

		write(text, new OutputStreamWriter(output));
	}

	/**
	 * Writes the given string to the {@link Writer}
	 * 
	 * @param text
	 *            The text to write
	 * @param writer
	 *            The output to write the text
	 */
	public static void write(String text, Writer writer) {
		copy(new StringReader(text), writer);
	}

	/**
	 * Delete file recursively.
	 * 
	 * @param file
	 *            The file to be deleted
	 */
	public static void delete(File file) {
		if (file == null || !file.exists())
			return;

		if (file.isFile()) {
			file.delete();
		} else if (file.isDirectory()) {
			File[] files = file.listFiles();
			for (int i = 0; i < files.length; i++)
				delete(files[i]);
		}

	}

	/**
	 * Gets an {@link OutputStream} for the given file
	 * 
	 * @param file
	 *            to get the {@link OutputStream}
	 * @return the output stream
	 */
	public static OutputStream outputStream(File file) {
		return outputStream(file, false);
	}

	/**
	 * Gets an {@link OutputStream} for the given file
	 * 
	 * @param file
	 *            to get the {@link OutputStream}
	 * @param append
	 *            flags if the next bytes should be appended to the file or the
	 *            file should be erased before write
	 * @return the output stream
	 */
	public static OutputStream outputStream(File file, boolean append) {
		try {
			notNull(file);
			if (!file.exists())
				file.createNewFile();

			if (!file.canWrite())
				throw new IOException("The file cannot be written: " + file);

			return new BufferedOutputStream(new FileOutputStream(file, append),
					BUFFER_SIZE);
		} catch (IOException e) {
			throw ExceptionUtils.wrapChecked(e);
		}
	}

	/**
	 * Gets an {@link InputStream} for the given file
	 * 
	 * @param file
	 *            to get the {@link InputStream}
	 * 
	 * @return the input stream
	 */
	public static InputStream inputStream(File file) {
		try {
			notNull(file);

			if (!file.exists())
				return null;

			if (!file.canRead())
				throw new IOException("The file cannot be read: " + file);

			return new BufferedInputStream(new FileInputStream(file),
					BUFFER_SIZE);
		} catch (IOException e) {
			throw ExceptionUtils.wrapChecked(e);
		}
	}
}
